#!/usr/bin/env python
# encoding: utf-8
"""
bkeepr.py

Created by Tristan on 2008-12-29.
Copyright (c) 2008 __MyCompanyName__. All rights reserved.
"""


# TODO
#
# Ignores unfinished books (apart from cumulative pages)
# Ignores bookmarks that aren't in order
# Only does latest 20 books
#
# Fix missing titles with LibraryThing? or OpenLibrary?
# LibraryThing support - don't think it has a user API though?
# GoodReads support

import sys
import os
import urllib2, urllib
import xml.dom.minidom
from datetime import datetime, timedelta
import random
import logging

from google.appengine.api import urlfetch
from google.appengine.ext import db

class AmazonBook(db.Model):
  isbn = db.StringProperty(required=True)
  pages = db.IntegerProperty(required=True)

class User(db.Model):
  username = db.StringProperty(required=True)
  requests = db.IntegerProperty(required=True)

class Book:
	# Represents a book read
	def __init__(self):	
		# Title
		self.title = ""
		# Author
		self.author = ""
		# ISBN
		self.isbn = ""
		# Start time (datetime)
		self.start = 0
		# End time (datetime)
		self.end = 0
		# Duration (timedelta)
		self.duration = timedelta()
		# Hash of bookmarks / notes
		self.bookmarks = []
		# From Amazon
		# Pages 
		self.pages = 350 # estimate of average
		self.original_language = ""
		self.publisher = ""
		self.publication_date = 0
		self.binding = ""
		self.deweydecimal = "" # can get fiction/non-fiction
	
	def addBookmark(self, page, note, time):
		# only allow bookmark if page < pages
		if self.pages != 0:
			if page < self.pages:
				bookmark = Bookmark()
				bookmark.page = page
				bookmark.note = note
				bookmark.time = time
				self.bookmarks.append(bookmark)
			else:
				print "Error in %s: trying to bookmark page outside length of book" % self.title
		# but we might not know the number of pages...
		else: 
			bookmark = Bookmark()
			bookmark.page = page
			bookmark.note = note
			bookmark.time = time
			self.bookmarks.append(bookmark)

	def tostring(self):
		days = "days"
		if max(self.duration.days,1)==1:
			days = "day"
		report = "%s by %s has %d pages, you started it on %s and read it in %s %s" % (self.title, self.author, self.pages, self.start.strftime("%d %B"), max(self.duration.days,1), days)		
		#progress = ""
		#for bookmark in self.bookmarks:
		#	progress = "%s\n   Bookmarked p.%s %s at %s" % (progress, bookmark.page, bookmark.note, bookmark.time)
		#amazon = "Published by %s in %s, binding = %s, DDN = %s, original languge = %s" % (self.publisher, self.publication_date, self.binding, self.deweydecimal, self.original_language)
		#return "%s%s\n%s" % (report, progress, amazon)
		return report

class Bookmark:
	# Represents a bookmark and/or note
	def __init__(self):
		# Page
		self.page = 0
		# Note
		self.note = ""
		# Time
		self.time = 0

class Event:
	# Represents a reading event (start/stop/bookmark)
	def __init__(self):
		# Page
		self.page = 0
		# Time
		self.time = 0
		# ISBN
		self.isbn = ""
		# Finished?
		self.finished = 0

class Bkkeepr:
	# Represents a user on bkkeepr
	def __init__(self, username, amazonAPIkey):
		logging.info("Request for %s" % username)									
		# Username on bkkeepr
		self.username = username
		# Update User record
		query = db.GqlQuery("SELECT * FROM User WHERE username = :thisusername ", thisusername = username)
		results = query.fetch(1)
		if len(results) > 0:
			results[0].requests = results[0].requests + 1 # Update requests
		else:
			# Create in Amazon DB
			user = User(username = username, requests = 1)
			user.put()		
		# List of Books, sorted by reading date
		self.booklist = []
		# List of Events, sorted by date
		self.eventlist = []
		self.google = 1 # running from Google App Engine
		self.usecache = 0 # will read and write from local cache rather than web, only for testing
		self.amazonAPIkey = amazonAPIkey
		self.maxBooks = 20 # Doing to many books can cause the Google Chart URL to be too long
	
	def readBkkeeprXML(self):
		# Read the list of books
		if (not self.google):
			if (not self.usecache):
				# cache file
				f = urllib2.urlopen("http://bkkeepr.com/api/user/%s.xml" % self.username)
				f2 = open('./cache/%s.xml' % self.username, 'w')
				for line in f:
					f2.write(line)
				f2.close()
				f.close()
				# Read from url
				f = urllib2.urlopen("http://bkkeepr.com/api/user/%s.xml" % self.username)
			else:
				# Read from cached file
				f = open('./cache/%s.xml' % self.username, 'r')
			return xml.dom.minidom.parse(f)
		else:
			f = urlfetch.fetch("http://bkkeepr.com/api/user/%s.xml" % self.username)
			if f.status_code == 200:
				return xml.dom.minidom.parseString(f.content)
			else:
				logging.error("Unable to get bkkeepr XML for %s, status_code = %d" % (username, f.status_code))	
				return
		
	
	# Reads the bkkeepr API, fills in info from the Amazon API
	def getBookData(self):
		books = {}
		self.booklist = []			
		self.eventlist = []
		
		dom = self.readBkkeeprXML()
		# Parse the <record> elements in the feed
		records = dom.getElementsByTagName('record')
		records.reverse()
		for record in records:
			title = self.getText(record.getElementsByTagName('title')[0].childNodes)
			author = self.getText(record.getElementsByTagName('author')[0].childNodes)
			isbn = self.getText(record.getElementsByTagName('isbn')[0].childNodes)
			timestring = self.getText(record.getElementsByTagName('time')[0].childNodes)
			time = datetime.strptime(timestring, "%Y-%m-%d %H:%M:%S")			
			page = self.getText(record.getElementsByTagName('page')[0].childNodes)
			note = self.getText(record.getElementsByTagName('note')[0].childNodes)

			if page == "0":
				# Started reading
				if isbn not in books:
					books[isbn] = Book()
					book = books[isbn]
					book.title = title
					book.author = author
					book.isbn = isbn
					book.start = time
					self.getInfoFromAmazon(book)
					# And create an Event for supporting simultaneous readings
					event = Event()
					event.isbn = isbn
					event.time = time
					self.eventlist.append(event)
					event.page = 0				
				else:
					# It's already been started so ignore this
					logging.error("Error in %s: Started book again - ignoring" % title)
			elif page == "finished":
				if isbn in books:
					book = books[isbn]
					if time > book.start:
						book.end = time
						book.duration = book.end - book.start
					# And create an Event for supporting simultaneous readings
					event = Event()
					event.isbn = isbn
					event.time = time
					self.eventlist.append(event)
					event.page = book.pages
					event.finished = 1
				else:
					logging.error("Error in %s: Ended book before it was started - ignoring" % title)
			else:
				# It's a bookmark and the book was started
				if isbn in books:
					book = books[isbn]
					if time > book.start:
						book.addBookmark(int(page),note,time)
					else:
						logging.error("Bookmark before time book was started it was started - ignoring")
					# And create an Event for supporting simultaneous readings
					event = Event()
					event.isbn = isbn
					event.time = time
					self.eventlist.append(event)
					event.page = int(page)
				else:
					logging.error("Bookmarked book before it was started - ignoring")

		# copy the dictionary of books into a list and sort	
		for isbn, book in books.iteritems():
			# only add finished books
			if (book.end is not 0):
				self.booklist.append(book)
		self.booklist.sort(cmp=lambda x,y: cmp(x.start, y.start))
		# Restrict to last maxBooks read
		self.booklist = self.booklist[-self.maxBooks:]
		# Return false if there were no completed books
		if len(self.booklist) < 1:
			return 0
		else:
			return 1

	# Returns a string of the form...
	def getStatistics(self):
		# calculate totals
		totaltime = timedelta();
		totalpages = 0	
		for book in self.booklist:
			totalpages = totalpages + book.pages
		totaltime = (self.booklist[-1].end - self.booklist[0].start) # from starting first book to finishing last
		# Create the string
		booksuffix = "s"
		if len(self.booklist) == 1:
			booksuffix = ""
		stats = "Over %s days you read %d book%s with %d pages in total.<br/>" % (max(totaltime.days,1), len(self.booklist), booksuffix, totalpages)
		#print "You were reading for %d days, %d hours, %d minutes and %d seconds" % (totaltime.days, totaltime.seconds/3600, (totaltime.seconds%3600)/60, (totaltime.seconds%60))	# 4h29m48s
		stats =  stats + ("There was an average of %d pages per book.<br/>" % (totalpages/len(self.booklist)))
		pagesuffix = "s"
		if totalpages/max(totaltime.days,1) == 1:
			pagesuffix = ""
		if float(totaltime.days*24)+(totaltime.seconds/3600) > 0:
			stats = stats + ("That's about %d page%s per day (or %.2f pages per hour).<br/>" % (totalpages/max(totaltime.days,1), pagesuffix, float(totalpages)/( float(totaltime.days*24)+(totaltime.seconds/3600) ) ))
		return stats
		
	# Returns a URL string that will draw a Google Chart of the user's reading
	# This plots a dataset per book read, showing pages read over time, resulting in a sawtooth pattern
	def getReadingGraph(self, chartsize_x, chartsize_y):
		maxpages = 0
		totalpages = 0
		for book in self.booklist:
			totalpages = totalpages + book.pages
			if book.pages > maxpages:
				maxpages = book.pages
		totaltime = self.booklist[-1].end - self.booklist[0].start

		# Saw-like graph showing velocity of each book read, separate series per book
		googlechart = "http://chart.apis.google.com/chart?"
		googlechart = googlechart + "chs=%sx%s" % (chartsize_x,chartsize_y)	
		googlechart = googlechart + "&cht=lxy"
		googlechart = googlechart + "&chd=t:"

		for book in self.booklist:
			xvalues = ""
			yvalues = ""
			start = book.start - self.booklist[0].start
			end = book.end - self.booklist[0].start
			# and then convert to days
			xvalues = "%s%.1f," % (xvalues,100*start.days/max(totaltime.days,1))
			yvalues = "%s%.1f," % (yvalues,0)
			# do bookmarks as well, but only if they are sequential in time
			lastbookmarktime = book.start
			totaldays = max(totaltime.days,1)
			for bookmark in book.bookmarks:
				if bookmark.time > lastbookmarktime:
					bookmarkdays = bookmark.time - self.booklist[0].start
					xvalues = "%s%.1f," % (xvalues,100*bookmarkdays.days/totaldays)
					yvalues = "%s%.1f," % (yvalues,100*bookmark.page/maxpages)
					lastbookmarktime = bookmark.time
			# and end of book
			xvalues = "%s%.1f,%.1f|" % (xvalues,100*end.days/totaldays, 100*end.days/totaldays)
			yvalues = "%s%.1f,%.1f|" % (yvalues, 100*book.pages/maxpages, 0)
			googlechart = googlechart + xvalues + yvalues
		# Remove trailing |
		googlechart = googlechart[:-1]

		# Add fill
		linecolour = "&chco=296E2D"
		linefill = "&chm="
		for bookct in range(len(self.booklist)):
			linefill = linefill + "B,296E2D77,%d,%d,0|" % (bookct, bookct+1)
		linefill = linefill[:-1]
		googlechart = googlechart + linefill + linecolour
		#googlechart = googlechart + "&chco=0000FF" + ("&chm=b,0000FF77,%d,%d,0" % (0, len(self.booklist)))

		# Add legend
		#legend = "&chdlp&chdl=" # label at bottom
		#for book in self.booklist:
		#	legend = legend + urllib.quote_plus(book.title.replace(":","")[:20].strip()) + "...|"
		#legend = legend[:-1]
		googlechart = googlechart + "&chtt=Books read for " + self.username

		# Add axis labels
		axis = "&chxt=x,y&chxl=0:"
		# x-axis	
		labeltime = self.booklist[0].start
		end = self.booklist[-1].end
		if labeltime.day < 15:
			# If start in first half of the month then print label for that month
			axis = axis + "|" + labeltime.strftime("%b")	
		labeltime = labeltime.replace(day=1) # to make sure it creates valid dates		
		while labeltime < end:
			if labeltime.month<12:
				labeltime = labeltime.replace(month=labeltime.month+1)
			else:
				labeltime = labeltime.replace(year=labeltime.year+1,month=1)
			axis = axis + "|" + labeltime.strftime("%b")	
		if end.day < 15:
			# If finish in first half of the month then remove the label for that month
			axis = axis[:len(axis)-4]	
		# y-axis
		axis = axis + "|1:|0|%dpp" % maxpages
		googlechart = googlechart + axis	
				
		# Dataset Labels
		#datalabels = "&chm="
		#bookct = 0
		#for book in self.booklist:
		#	booktitle = urllib.quote_plus(book.title.replace(":","")[:10].strip()) + "..."
		#	datalabels = datalabels + "t%s,55555577,%d,0,10|" % (booktitle,bookct)
		#	bookct = bookct + 1
		#datalabels = datalabels[:-1]
		#googlechart = googlechart + datalabels
							
		if  (len(googlechart)>2047):
			return "There was a problem generating the graph, sorry."			
		return googlechart
				
		
	# Returns a URL string that will draw a Google Chart of the user's reading
	# This is a cumulative graph showing total pages read over time
	def getCumulativeReadingGraph(self, chartsize_x, chartsize_y):
		# Cumulative graph showing progress of all books read, just one series
		# This won't work with simultaneous books, for that you'd need to extract all the records and sort by time
		cumulativepages = 0
		totalpages = 0		
		xvalues = ""
		yvalues = ""
		currentlyreading = {}
		bookstarts = {}
		totaltime = self.booklist[-1].end - self.booklist[0].start		
		for book in self.booklist:
			totalpages = totalpages + book.pages

		googlechart = "http://chart.apis.google.com/chart?"
		googlechart = googlechart + "chs=%sx%s" % (chartsize_x, chartsize_y)	
		googlechart = googlechart + "&cht=lxy"
		googlechart = googlechart + "&chd=t:"
		
		# Dataset Labels
		datalabels = "&chm="
		eventct = 0
		bookct = 1
		for event in self.eventlist:
			if event.page == 0:
				currentlyreading[event.isbn] = 0
				#datalabels = datalabels + "t%d,55555577,0,%d,10|" % (bookct,eventct)	# Labels
				datalabels = datalabels + "o,999999,0,%d,5|" % (eventct)	# Symbols			
				bookct = bookct + 1
			elif event.finished:
				if currentlyreading.has_key(event.isbn):
					cumulativepages = cumulativepages + (event.page - currentlyreading[event.isbn])
					del currentlyreading[event.isbn]
			else:
				# bookmark
				if currentlyreading.has_key(event.isbn):			
					cumulativepages = cumulativepages + (event.page - currentlyreading[event.isbn])
					currentlyreading[event.isbn] = event.page
			timediff = event.time - self.booklist[0].start
			xvalues = xvalues + "%.1f," % (100*timediff.days/max(totaltime.days,1))
			yvalues = yvalues + "%.1f," % (100*cumulativepages/totalpages)
			eventct = eventct + 1
		xvalues = xvalues[:-1] # Remove trailing ,
		yvalues = yvalues[:-1] # Remove trailing ,
		googlechart = googlechart + xvalues + "|" + yvalues		
		
		# Data point labels
		datalabels = datalabels[:-1]
		googlechart = googlechart + datalabels		

		googlechart = googlechart + "&chco=296E2D"		
		googlechart = googlechart + "&chtt=Pages read for " + self.username		
		
		# Add axis labels
		axis = "&chxt=x,y&chxl=0:"
		# x-axis	
		labeltime = self.booklist[0].start
		end = self.booklist[-1].end
		if labeltime.day < 15:
			# If start in first half of the month then print label for that month
			axis = axis + "|" + labeltime.strftime("%b")	
		labeltime = labeltime.replace(day=1) # to make sure it creates valid dates		
		while labeltime < end:
			if labeltime.month<12:
				labeltime = labeltime.replace(month=labeltime.month+1)
			else:
				labeltime = labeltime.replace(year=labeltime.year+1,month=1)
			axis = axis + "|" + labeltime.strftime("%b")	
		if end.day < 15:
			# If finish in first half of the month then remove the label for that month
			axis = axis[:len(axis)-4]	
		# y-axis
		axis = axis + "|1:|0|%dpp" % cumulativepages		
		googlechart = googlechart + axis	

		if  (len(googlechart)>2047):
			return "There was a problem generating the graph, sorry."					
		return googlechart
		
		
	def readAmazonXML(self, book):
		# Read the list of books
		logging.info("Getting Amazon XML for %s" % book.title)
		if (not self.google):
			if (not self.usecache):
				# cache file
				f = urllib2.urlopen("http://ecs.amazonaws.com/onca/xml?Service=AWSECommerceService&AWSAccessKeyId=%s&Operation=ItemLookup&&SearchIndex=Books&IdType=ISBN&ItemId=%s&ResponseGroup=ItemAttributes" % (self.amazonAPIkey, book.isbn) )
				f2 = open('./cache/%s' % book.isbn, 'w')
				for line in f:
					f2.write(line)
				f2.close()
				f.close()
				# Read from url
				f = urllib2.urlopen("http://ecs.amazonaws.com/onca/xml?Service=AWSECommerceService&AWSAccessKeyId=%s&Operation=ItemLookup&&SearchIndex=Books&IdType=ISBN&ItemId=%s&ResponseGroup=ItemAttributes" % (self.amazonAPIkey, book.isbn))
			else:
				# Read from cached file
				f = open('./cache/%s' % book.isbn, 'r')		
			return xml.dom.minidom.parse(f)
		else:
			f = urlfetch.fetch("http://ecs.amazonaws.com/onca/xml?Service=AWSECommerceService&AWSAccessKeyId=%s&Operation=ItemLookup&&SearchIndex=Books&IdType=ISBN&ItemId=%s&ResponseGroup=ItemAttributes" % (self.amazonAPIkey, book.isbn))
			if f.status_code == 200:
				return xml.dom.minidom.parseString(f.content)
			else:
				logging.error("Error getting XML from Amazon for %s, status_code = %d" % (book.isbn, f.status_code))
				return
			
	# Looks up Book in Amazon API and fills in fields
	# It caches this in the Google App data store for subsequent lookups
	def getInfoFromAmazon(self, book):
		query = db.GqlQuery("SELECT * FROM AmazonBook WHERE isbn = :isbn ", isbn = book.isbn)
		results = query.fetch(1)
		if len(results) > 0:
			book.pages = results[0].pages
		else:
			dom = self.readAmazonXML(book)
			pages = dom.getElementsByTagName('NumberOfPages')
			if len(pages) > 0:
				book.pages = int(self.getText(pages[0].childNodes))
			publisher = dom.getElementsByTagName('Publisher')
			if len(publisher) > 0:
				book.publisher = self.getText(publisher[0].childNodes)
			publication_date = dom.getElementsByTagName('PublicationDate')
			if len(publication_date) > 0:
				# Can't be sure of format
				#book.publication_date = datetime.strptime(getText(publication_date[0].childNodes), "%Y-%m-%d")
				book.publication_date = self.getText(publication_date[0].childNodes)
			binding = dom.getElementsByTagName('Binding')
			if len(binding) > 0:
				book.binding = self.getText(binding[0].childNodes)
			deweydecimal = dom.getElementsByTagName('DeweyDecimalNumber')
			if len(deweydecimal) > 0:
				book.deweydecimal = self.getText(deweydecimal[0].childNodes)
			languages = dom.getElementsByTagName('Language')
			for language in languages:
				name = language.getElementsByTagName('Name')
				typeelement = language.getElementsByTagName('Type')
				if len(typeelement) > 0:
					type_string = self.getText(typeelement[0].childNodes)
					if type_string == "Original Language":
						if len(name) > 0:
							book.original_language = self.getText(name[0].childNodes)
			# Create in Amazon DB
			amazonBook = AmazonBook(isbn = book.isbn, pages = book.pages)
			amazonBook.put()
			
	# Gets text from XML nodelist
	def getText(self, nodelist):
		rc = ""
		for node in nodelist:
			if node.nodeType == node.TEXT_NODE:
				rc = rc + node.data
		return rc


	
def main():
	# tristanf, fidothe, rooreynolds, fabricoffolly, tomtaylor	
	amazonAPIKey = "1S64ZQMTY5GKZH4RMF82"
	bkkeepr = Bkkeepr("tristanf", amazonAPIKey)
	bkkeepr.usecache = 1
	bkkeepr.google = 0
	bkkeepr.getBookData()
	# Print out list of books
	for book in bkkeepr.booklist:
		print book.tostring()
		print"\n"
	print bkkeepr.getStatistics()
	print bkkeepr.getReadingGraph(600,200)
	
	print bkkeepr.getCumulativeReadingGraph(600,200)

if __name__ == '__main__':
	main()

